Haibo Zhou's site

Mobile Development Articles

Apply Multithreading in CoreData.

CoreData is a native framework provided by Apple in macOS and iOS for data persistent use. If you have known how to use CoreData and want to involve Multithreading support on it. This article is for you.

By default, when you use the persistentContainer.viewContext, the one is in your AppDelegate CoreData stack or other file if you implement your own CoreData stack. Try to option click that viewContext, you would see "The managed object context associated with the main queue." That means this MOC(managed object context) is on main queue aka main thread. Therefore anything you do on this MOC(viewContext) will be on main queue, which is not good.

Although many tutorials would use CoreData's context this way UIApplication.shared.persistentContainer.viewContext, use the share persistentContainer in UIApplication for the sake of simplicity. I think it is not a good practice, because that viewContext is on main queue. Then what we should do?

In general, avoid doing data processing on the main queue that is not user-related. Data processing can be CPU-intensive, and if it is performed on the main queue, it can result in unresponsiveness in the user interface. If your application will be processing data, such as importing data into Core Data from JSON, create a private queue context and perform the import on the private context.

Apple doc, Using a Private Queue to Support Concurrency

Then how we could create that private queue, see below code

class CoreDataManager {
    // 1
    let managedObjectContext: NSManagedObjectContext
    // 2
    private let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    let coreDataStack = CoreDataStack()
    
    // 3
    static let shared = CoreDataManager()
    
    // 4
    private init() {
        self.managedObjectContext = coreDataStack.persistentContainer.viewContext
        privateMOC.parent = self.managedObjectContext
    }
  1. In this CoreDataManager struct, create a moc(NSManagedObjectContext) as our main context.
  2. Create a privateMOC with .privateQueueConcurrencyType
  3. Create a Singleton pattern. In short, Singleton creates a unique shared class instance for global use across your app.
  4. In this private initializer, we assign moc with coreDataStack.persistentContainer.viewContext, and set it to privateMOC's parent.

 

Notice, I would suggest to create your own coreDataStack, just copy the CoreData related code in AppDelegate to your own coreDataStack class. Why, because for 'UIApplication.shared, it is on main queue. Then you might get an anoying tricky problem about how to let it work on background queue.

Okay, now we have created a parent moc on main queue and its child moc on private queue.

you may wonder what is PrivateQueueConcurrencyType

The NSPrivateQueueConcurrencyType configuration creates its own queue upon initialization and can be used only on that queue. Because the queue is private and internal to the NSManagedObjectContext instance, it can only be accessed through the performBlock: and the performBlockAndWait: methods.

Then how to use this privateMOC to do the task. I give a example here:

// 1
func fetchAllPlaylists(completion: @escaping ([Playlist]?) -> Void) {
    privateMOC.performAndWait {
        do {
            let playlists: [Playlist] = try privateMOC.fetch(Playlist.fetchRequest())
            // 2
            printThreadStats()
            // 3
            completion(playlists)
        } catch {
            print("fetchAllPlaylists failed, \(error), \(error.localizedDescription)")
            completion(nil)
        }
    }
}

// 4
func createPlaylist(name: String) {
    privateMOC.performAndWait {
        let newPlaylist = Playlist(context: privateMOC)
        newPlaylist.name = name
        // 5
        synchronize()
    }
}

func deleteSong(songRecord: SongTable) {
    privateMOC.performAndWait {
        privateMOC.delete(songRecord)
        synchronize()
    }
}

func updatePlaylist(playlist: Playlist, newName: String) {
    privateMOC.performAndWait {
        playlist.name = newName
        synchronize()
    }
}

func synchronize() {
    do {
        // We call save on the private context, which moves all of the changes into the main queue context without blocking the main queue.
        try privateMOC.save()
        managedObjectContext.performAndWait {
            do {
                try managedObjectContext.save()
            } catch {
                print("Could not synchonize data. \(error), \(error.localizedDescription)")
            }
        }
    } catch {
        print("Could not synchonize data. \(error), \(error.localizedDescription)")
    }
}

func printThreadStats() {
    if Thread.isMainThread {
        print("on the main thread")
    } else {
        print("off the main thread")
    }
}

 

  1. Let's say I have a entity called Playlist and fetchAllPlaylists would fetch all the records from this entity. We use performAndWait function here to run closure code in private context queue.
  2. printThreadStats is little helper function let me know which thread I'm currently in.
  3. Notice! here we are using completion handler(@escaping closure) to pass the results to the caller, this technique is called call back. I will not cover it to much here, but in short completion will be executed after fetchAllPlaylists function is returned, you could think completion is outside of this function and passing to caller.
  4. Here we create a new Playlist record and give it a name.
  5. In this phase, the data is changed, so we should save the change into context. That is what synchronize() do. We first save it on private context, then save it on main context(parent). Finially, the CoreData saving is completed. And we reuse this synchronize when we need to save the changes as functional programming's purpose(reuse code).

 

OK, get tired? We almost done, last step is to call those API.

override func viewDidLoad() {
    super.viewDidLoad()
    
    // 1
    DispatchQueue.global(qos: .background).async { [weak self] in
        self?.fetchAndReload()
    }
}

private func fetchAndReload() {
    // 2
    CoreDataManager.shared.fetchAllPlaylists(completion: { playlists in
        guard let playlists = playlists else { return }
        self.playlists = playlists
    })
    
    DispatchQueue.main.async { [weak self] in
        self?.tableView.reloadData()
    }
}

 

  1. We fetch all playlists in viewDidLoad on background queue, usually we do data loading here.
  2. Then get the playlists with completion closure, and after data is retrieved, populated the data on tableView on main queue. Remember all UI related operation must be on main thread.

   

Well, that is all. Hope this article could help some guys, see you.

Tagged with: